package com.gobang.demo.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gobang.demo.game.OnlineUserManager;
import com.gobang.demo.game.Room;
import com.gobang.demo.game.RoomManager;
import com.gobang.demo.mapper.UserMapper;
import com.gobang.demo.model.GameReadyResponse;
import com.gobang.demo.model.PutChessRequest;
import com.gobang.demo.model.PutChessResponse;
import com.gobang.demo.model.User;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Component
@Slf4j
// 通过这个类来处理游戏功能中的 websocket 请求
public class GameApi extends TextWebSocketHandler {
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private RoomManager roomManager;
    @Autowired
    private OnlineUserManager onlineUserManager;
    @Autowired
    private UserMapper userMapper;

    @Override
    // WebSocket 连接建立后处理的逻辑
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 1. 先获取到用户的身份信息. (从 HttpSession 里拿到当前用户的对象)
        User user = (User) session.getAttributes().get("user");
        GameReadyResponse response = new GameReadyResponse();
        if (user == null) {
            log.info("玩家未登录");
        }
        // 2. 判定当前用户是否已经进入房间. (拿着房间管理器进行查询)
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null){
            // 如果为 null, 当前没有找到对应的房间. 该玩家还没有匹配到.
            log.info("玩家未匹配");
            response.setOk(false);
            response.setReason("玩家未匹配");
            // 给前端返回信息
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
            return;
        }
        // 3. 判定当前是不是多开 (该用户是不是已经在其他地方进入游戏了)
        //    前面准备了一个 OnlineUserManager
        if (onlineUserManager.getFromGameHall(user.getUserId()) != null ||
             onlineUserManager.getFromGameRoom(user.getUserId()) != null) {
            // 如果一个账号, 一边是在游戏大厅, 一边是在游戏房间, 也视为多开
            log.info("玩家多开");
            response.setOk(true);
            response.setReason("玩家多开");
            response.setMessage("repeatConnection");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }
        // 4. 设置当前玩家上线!
        onlineUserManager.enterGameRoom(user.getUserId(), session);
        log.info("玩家{}进入游戏房间",user.getUserName());
        // 5. 把玩家加入到游戏房间, 由于玩家1与玩家2是两个不同的线程，存在线程安全，需要加锁
        synchronized (room) {
            if (room.getUser1() == null) {
                room.setUser1(user);
                // 谁先进房间谁就执白子
                room.setWhiteUser(user.getUserId());
                log.info("玩家{}准备就绪",user.getUserName());
                return;
            }
            if (room.getUser2() == null) {
                room.setUser2(user);
                log.info("玩家{}准备就绪",user.getUserName());
            }
        }
        // 当两个玩家都加入成功之后, 就要让服务器, 给这两个玩家都返回 websocket 的响应数据.
        // 通知这两个玩家, 游戏双方都已经准备好了.
        noticeGameReady(room,room.getUser1(),room.getUser2());
        noticeGameReady(room,room.getUser2(),room.getUser1());
    }

    @Override
    // WebSocket 连接出现异常后处理的逻辑
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            return;
        }
        WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());
        if (exitSession == session) {
            onlineUserManager.exitGameRoom(user.getUserId());
            log.info("房间异常，玩家{}离开游戏房间",user.getUserName());
        }
        // 连接异常退出通知对方获胜
        noticeThatUserWin(user);
    }

    @Override
    // WebSocket 连接关闭后处理的逻辑
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            return;
        }
        WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());
        if (exitSession == session) {
            onlineUserManager.exitGameRoom(user.getUserId());
            log.info("房间关闭，玩家{}离开游戏房间",user.getUserName());
        }
        // 游戏退出通知对方获胜
        noticeThatUserWin(user);
    }
    @Override
    // 处理前端的接收到的文本消息
    // 接收到的是前端发过来的落子位置
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 1. 先从 session 里拿到当前用户的身份信息
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            return;
        }
        // 2. 根据玩家 id 获取到房间对象
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            return;
        }
        // 通过这个方法来处理一次落子操作
        // 返回的数组arr[0]--胜者id  arr[1]--败者id
        int[] arr = putChess(room, message.getPayload());
        if (arr[0] > 0 && arr[1] > 0) {
            int winner = arr[0];
            int loser = arr[1];
            // 将对局结果写入数据库
            bonusPoint(winner);
            markDown(loser);
        }
    }
    //  websocket 的响应数据.通知这两个玩家说, 游戏双方都已经准备好了.
    @SneakyThrows
    private void noticeGameReady(Room room, User thisUser, User thatUser) {
        GameReadyResponse response = new GameReadyResponse();
        response.setOk(true);
        response.setRoomId(room.getRoomId());
        response.setMessage("gameReady");
        response.setThisUserId(thisUser.getUserId());
        response.setThatUserId(thatUser.getUserId());
        response.setWhiteUser(room.getWhiteUser());
        // 把当前的响应数据传回给玩家.
        // 根据 id 从管理器取用户对应的 webSocketSession
        WebSocketSession session = onlineUserManager.getFromGameRoom(thisUser.getUserId());
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
    }
    // 要是玩家掉线或退出通知对手获胜
    @SneakyThrows
    private void noticeThatUserWin(User user) {
        PutChessResponse response = new PutChessResponse();
        // 1. 根据当前玩家, 找到玩家所在的房间
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            log.info("房间不存在");
            return;
        }
        // 2. 根据房间找到对手
        User thatUser = (user == room.getUser1()) ? room.getUser2() : room.getUser1();
        // 3. 找到对手的在线状态
        WebSocketSession session = onlineUserManager.getFromGameRoom(thatUser.getUserId());
        // 对手也掉线
        if (session == null) {
            return;
        }
        // 通知前端获胜者
        response.setWinner(thatUser.getUserId());
        response.setMessage("drop");
        response.setUserId(thatUser.getUserId());
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        log.info("玩家{}掉线，玩家{}获胜",user.getUserName(),thatUser.getUserName());
        roomManager.remove(room, user.getUserId(), thatUser.getUserId());
        bonusPoint(thatUser.getUserId());
        markDown(user.getUserId());
    }

    @SneakyThrows
    // 通过这个方法来处理一次落子操作.
    private int[] putChess(Room room, String payload) {
        // 定义一个数组来保存胜利者id与失败者id
        int[] victoryAndDefeat = new int[2];
        // 将前端发来的数据转成对象
        PutChessRequest request = objectMapper.readValue(payload,PutChessRequest.class);
        System.out.println(request);
        PutChessResponse response = new PutChessResponse();
        if (!request.getMessage().equals("putChess")) {
            log.info("错误请求");
            return victoryAndDefeat;
        }
        // 通过房间获取游戏玩家
        User user1 = room.getUser1();
        User user2 = room.getUser2();
        // 获取落子位置
        Integer row = request.getRow();
        Integer col = request.getCol();
        // 获取当前房间的棋盘
        int[][] board = room.getBoard();
        // 在 board 里玩家1的落子用1表示, 玩家2的落子用2表示
        int chess = user1.getUserId() == request.getUserId() ? 1 : 2;
        if (board[row][col] != 0) {
            log.info("该位置{},{}已经有子",row,col);
            return victoryAndDefeat;
        }
        board[row][col] = chess;
        room.setBoard(board);
        // 判断胜负
        int winner = checkWinner(room,chess,row,col);
        // 不为 0 胜负已分,返回胜者id
        if (winner != 0) {
            winner = chess == 1 ? user1.getUserId() : user2.getUserId();
            victoryAndDefeat[0] = winner;
            victoryAndDefeat[1] = user1.getUserId() == winner ? user2.getUserId() : user1.getUserId();
        }
        // 给房间中的所有客户端都返回响应.
        response.setMessage("putChess");
        response.setUserId(request.getUserId());
        response.setCol(col);
        response.setRow(row);
        response.setWinner(winner);
        WebSocketSession session1 = onlineUserManager.getFromGameRoom(user1.getUserId());
        WebSocketSession session2 = onlineUserManager.getFromGameRoom(user2.getUserId());
        if (session1 == null) {
            log.info("玩家{}掉线",user1.getUserName());
        }
        if (session2 == null) {
            log.info("玩家{}掉线",user2.getUserName());
        }
        if (winner != 0) {
            log.info("胜负已分");
            roomManager.remove(room, user1.getUserId(),user2.getUserId());
        }
        session1.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        session2.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));

        return victoryAndDefeat;
    }

    // 使用这个方法来判定当前落子是否分出胜负.
    // 约定如果玩家1 获胜, 就返回玩家1 的 userId
    // 如果玩家2 获胜, 就返回玩家2 的 userId
    // 如果胜负未分, 就返回 0
    private int checkWinner(Room room,Integer chess,Integer row, Integer col) {
        // 获取棋盘信息
        int[][] board = room.getBoard();
        // 1. 检查所有的行
        //    先遍历这五种情况
        for (int c = col - 4; c <= col;c++) {
          // 用try  catch 来处理数组越界异常，要是数组越界直接跳过继续
          try {
              // 针对其中的一种情况, 来判定这五个子是不是连在一起了~
              // 不光是这五个子得连着, 而且还得和玩家落的子是一样~~ (才算是获胜)
              if (board[row][c] == chess &&
                      board[row][c + 1] == chess &&
                      board[row][c + 2] == chess &&
                      board[row][c + 3] == chess &&
                      board[row][c + 4] == chess) {
                  return chess;
              }
          }catch (ArrayIndexOutOfBoundsException e){
              // 如果出现数组下标越界的情况, 就在这里直接忽略这个异常.
              continue;
          }
        }
        // 2. 检查所有列
        for (int r = row - 4; r <= row;r++) {
            try {
                if (board[r][col] == chess &&
                        board[r + 1][col] == chess &&
                        board[r + 2][col] == chess &&
                        board[r + 3][col] == chess &&
                        board[r + 4][col] == chess) {
                    return chess;
                }
            }catch (ArrayIndexOutOfBoundsException e){
                continue;
            }
        }
        // 3. 检查左对角线
        for (int c = col - 4,r = row - 4; c <= col && r <= row;c++,r++) {
            try {
                if (board[r][c] == chess &&
                        board[r + 1][c + 1] == chess &&
                        board[r + 2][c + 2] == chess &&
                        board[r + 3][c + 3] == chess &&
                        board[r + 4][c + 4] == chess) {
                    return chess;
                }
            }catch (ArrayIndexOutOfBoundsException e){
                continue;
            }
        }
        // 4. 检查右对角线
        for (int c = col - 4,r = row + 4; c <= col && r >= row;c++,r--) {
            try {
                if (board[r][c] == chess &&
                        board[r - 1][c + 1] == chess &&
                        board[r - 2][c + 2] == chess &&
                        board[r - 3][c + 3] == chess &&
                        board[r - 4][c + 4] == chess) {
                    return chess;
                }
            }catch (ArrayIndexOutOfBoundsException e){
                continue;
            }
        }
        // 胜负未分, 就直接返回 0 了.
        return 0;
    }
    // 将胜者,败者的积分写入数据库
    private void bonusPoint(int winner) {
        userMapper.bonusPoint(winner);
    }
    private void  markDown (int loser) {
        userMapper.markDown(loser);
    }
}
